using System;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Security;
using Microsoft.Web.Administration;
using LeastPrivilege.CustomBasicAuthentication.Management;

namespace LeastPrivilege.CustomBasicAuthentication
{
    public partial class CustomBasicAuthenticationModule
    {
        private CustomBasicAuthenticationSection _configuration = null;
        string _cachePrefix = "_lp_CustomBasicAuth_";
        
        #region Properties
        protected CustomBasicAuthenticationSection Configuration
        {
            get
            {
                if (_configuration == null)
                {
                    _configuration = (CustomBasicAuthenticationSection)
                        WebConfigurationManager.GetSection(
                            HttpContext.Current,
                            CustomBasicAuthenticationSection.SectionName,
                            typeof(CustomBasicAuthenticationSection));

                }

                // TODO: error handling
                return _configuration;
            }
        }

        protected bool IsAnonymousAllowed
        {
            get
            {
                AnonymousAuthenticationSection anon = (AnonymousAuthenticationSection)
                    WebConfigurationManager.GetSection(
                        HttpContext.Current,
                        AnonymousAuthenticationSection.SectionName,
                        typeof(AnonymousAuthenticationSection));

                return anon.Enabled;
            }
        }

        protected MembershipProvider Provider
        {
            get
            {
                if (string.Equals(Configuration.ProviderName, "default", StringComparison.OrdinalIgnoreCase))
                    return Membership.Provider;
                else
                    return Membership.Providers[Configuration.ProviderName];
            }
        }

        protected bool IsHeaderPresent
        {
            get
            {
                HttpContext context = HttpContext.Current;
                string authHeader = context.Request.Headers["Authorization"];
                return (!string.IsNullOrEmpty(authHeader));
            }
        }
        #endregion

        #region Helper
        private bool AuthenticateUser()
        {
            string username = "", password = "";
            string authHeader = HttpContext.Current.Request.Headers["Authorization"];

            if (authHeader != null && authHeader.StartsWith("Basic"))
            {
                // extract credentials from header
                string[] credentials = ExtractCredentials(authHeader);
                username = credentials[0];
                password = credentials[1];

                if (IsCredentialCached(username, password))
                {
                    SetPrincipal(username);
                    return true;
                }
                else if (Provider.ValidateUser(username, password))
                {
                    if (Configuration.CachingEnabled)
                    {
                        CacheCredential(username, password);
                    }

                    SetPrincipal(username);
                    new AuthenticationSuccessEvent(this, username).Raise();

                    return true;
                }
            }

            new AuthenticationFailureEvent(this, username).Raise();
            return false;
        }

        // set a 401 and end the current request
        // processing will jump directly to EndRequest
        private static void DenyAccess()
        {
            HttpContext context = HttpContext.Current;

            context.Response.StatusCode = 401;
            context.Response.End();
        }

        // caches a credential identifier
        private void CacheCredential(string username, string password)
        {
            string identifier = GetIdentifier(username, password);

            try
            {
                HttpContext.Current.Cache.Add(
                    identifier,
                    "ok",
                    null,
                    DateTime.Now.AddMinutes(Configuration.CachingDuration),
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Normal,
                    null);

                new CredentialCacheAddEvent(this, username).Raise();
            }
            catch (Exception ex)
            {
                new CredentialCacheAddErrorEvent(this, username, ex).Raise();
            }
        }

        // checks if the credential has been cached already
        private bool IsCredentialCached(string username, string password)
        {
            if (!Configuration.CachingEnabled)
                return false;

            string identifier = GetIdentifier(username, password);
            bool cacheHit = (HttpContext.Current.Cache[identifier] != null);

            if (cacheHit)
            {
                new CredentialCacheHitEvent(this, username).Raise();
            }
            else
            {
                new CredentialCacheMissEvent(this, username).Raise();
            }

            return cacheHit;
        }

        // create GenericPrincipal and set it on Context.User
        private static void SetPrincipal(string username)
        {
            // create principal and set Context.User
            GenericIdentity id = new GenericIdentity(username, "CustomBasic");
            GenericPrincipal p = new GenericPrincipal(id, null);
            HttpContext.Current.User = p;
        }

        // send header to start Basic Authentication handshake
        private void SendAuthenticationHeader()
        {
            HttpContext context = HttpContext.Current;

            context.Response.StatusCode = 401;
            context.Response.AddHeader(
                "WWW-Authenticate",
                String.Format("Basic realm=\"{0}\"", Configuration.Realm));
        }

        // extracts and decodes username and password from the auth header
        private string[] ExtractCredentials(string authHeader)
        {
            // strip out the "basic"
            string encodedUserPass = authHeader.Substring(6).Trim();

            // that's the right encoding
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
            int separator = userPass.IndexOf(':');

            string[] credentials = new string[2];
            credentials[0] = userPass.Substring(0, separator);
            credentials[1] = userPass.Substring(separator + 1);

            return credentials;
        }

        // create a string identifier for the cache key
        // format: prefix + Base64(Hash(username+password))
        private string GetIdentifier(string username, string password)
        {
            // use default hash algorithm configured on this machine via CryptoMappings
            // usually SHA1CryptoServiceProvider
            HashAlgorithm hash = HashAlgorithm.Create();

            string identifier = username + password;
            byte[] identifierBytes = Encoding.UTF8.GetBytes(identifier);
            byte[] identifierHash = hash.ComputeHash(identifierBytes);

            return _cachePrefix + Convert.ToBase64String(identifierHash);
        }
        #endregion
    }
}